# PooledHiLoSequencePersister.java

    package org.codefilarete.stalactite.mapping.id.sequence.hilo;

    import java.sql.Connection;
    import java.util.Map;

    import org.codefilarete.reflection.DefaultReadWritePropertyAccessPoint;
    import org.codefilarete.reflection.ReadWritePropertyAccessPoint;
    import org.codefilarete.stalactite.engine.SeparateTransactionExecutor;
    import org.codefilarete.stalactite.engine.runtime.BeanPersister;
    import org.codefilarete.stalactite.mapping.DefaultEntityMapping;
    import org.codefilarete.stalactite.mapping.id.manager.AlreadyAssignedIdentifierManager;
    import org.codefilarete.stalactite.mapping.id.sequence.hilo.PooledHiLoSequencePersister.Sequence;
    import org.codefilarete.stalactite.mapping.id.sequence.hilo.PooledHiLoSequencePersister.SequenceTable;
    import org.codefilarete.stalactite.sql.ConnectionConfiguration.ConnectionConfigurationSupport;
    import org.codefilarete.stalactite.sql.Dialect;
    import org.codefilarete.stalactite.sql.ddl.structure.Column;
    import org.codefilarete.stalactite.sql.ddl.structure.Database.Schema;
    import org.codefilarete.stalactite.sql.ddl.structure.Table;
    import org.codefilarete.tool.collection.Maps;

    /**
     * Persister dedicated to {@link Sequence}.
     *
     * The same instance can be shared, as long as each calls {@link #reservePool(String, int)} with a different name parameter to avoid
     * sequence name collision.
     *
     * @author Guillaume Mary
     */
    public class PooledHiLoSequencePersister extends BeanPersister<Sequence, String, SequenceTable> {

    	/**
    	 * Constructor with default table and column names.
    	 * @param dialect the {@link Dialect} to use for database dialog
    	 * @param separateTransactionExecutor a transaction provider that must give a new and separate transaction
    	 * @param jdbcBatchSize the JDBC batch size, not really useful for this class since it doesn't do massive insert
    	 * @see PooledHiLoSequenceStorageOptions#DEFAULT
    	 */
    	public PooledHiLoSequencePersister(Dialect dialect, SeparateTransactionExecutor separateTransactionExecutor, int jdbcBatchSize) {
    		this(PooledHiLoSequenceStorageOptions.DEFAULT, dialect, separateTransactionExecutor, jdbcBatchSize);
    	}

    	public PooledHiLoSequencePersister(PooledHiLoSequenceStorageOptions storageOptions, Dialect dialect, SeparateTransactionExecutor separateTransactionExecutor, int jdbcBatchSize) {
    		// we reuse default PersistentContext
    		super(new SequencePersisterConfigurer().buildConfiguration(storageOptions),
    				dialect, new ConnectionConfigurationSupport(separateTransactionExecutor, jdbcBatchSize));
    	}

    	public long reservePool(String sequenceName, int poolSize) {
    		SequenceBoundJdbcOperation jdbcOperation = new SequenceBoundJdbcOperation(sequenceName, poolSize);
    		// the operation is executed in a new and parallel transaction in order to manage concurrent accesses
    		((SeparateTransactionExecutor) getConnectionProvider()).executeInNewTransaction(jdbcOperation);
    		return jdbcOperation.getUpperBound();
    	}

    	public static class SequenceTable extends Table<SequenceTable> {

    		private final Column<SequenceTable, Long> nextValColumn;
    		private final Column<SequenceTable, String> sequenceNameColumn;

    		public SequenceTable(Schema schema, String name, String sequenceNameColName, String nextValColName) {
    			super(schema, name);
    			sequenceNameColumn = addColumn(sequenceNameColName, String.class);
    			sequenceNameColumn.setPrimaryKey(true);
    			nextValColumn = addColumn(nextValColName, long.class);
    		}

    		public Map<ReadWritePropertyAccessPoint<Sequence, Object>, Column<SequenceTable, Object>> getPooledSequenceFieldMapping() {
    			return (Map) Maps
    					.forHashMap(ReadWritePropertyAccessPoint.class, Column.class)
    					.add(Sequence.SEQUENCE_NAME_FIELD, sequenceNameColumn)
    					.add(Sequence.VALUE_FIELD, nextValColumn);
    		}
    	}

    	/**
    	 * POJO which represents a line in the sequence table which is composed of 2 columns: one for the name of the sequence, the second for its value.
    	 */
    	public static class Sequence {

    		private static final ReadWritePropertyAccessPoint<Sequence, String> SEQUENCE_NAME_FIELD;
    		private static final ReadWritePropertyAccessPoint<Sequence, Long> VALUE_FIELD;

    		static {
    			SEQUENCE_NAME_FIELD = DefaultReadWritePropertyAccessPoint.fromMethodReference(Sequence::getSequenceName, Sequence::setSequenceName);
    			VALUE_FIELD = DefaultReadWritePropertyAccessPoint.fromMethodReference(Sequence::getStep, Sequence::setStep);
    		}

    		private String sequenceName;
    		private long step;

    		private Sequence() {
    		}

    		public Sequence(String sequenceName) {
    			this.sequenceName = sequenceName;
    		}

    		public Sequence(String sequenceName, long step) {
    			this.sequenceName = sequenceName;
    			this.step = step;
    		}

    		public long getStep() {
    			return step;
    		}

    		public void setStep(long step) {
    			this.step = step;
    		}

    		public String getSequenceName() {
    			return sequenceName;
    		}

    		public void setSequenceName(String sequenceName) {
    			this.sequenceName = sequenceName;
    		}
    	}

    	/**
    	 * Class aimed at executing update operations of {@link Sequence}.
    	 */
    	private class SequenceBoundJdbcOperation implements SeparateTransactionExecutor.JdbcOperation {
    		private final String sequenceName;
    		private final int stepSize;
    		private Sequence sequence;

    		private SequenceBoundJdbcOperation(String sequenceName, int nextStepSize) {
    			this.sequenceName = sequenceName;
    			this.stepSize = nextStepSize;
    		}

    		@Override
    		public void execute(Connection currentSeparateConnection) {
    			sequence = readStep(sequenceName);
    			if (sequence != null) {
    				sequence.setStep(sequence.getStep() + stepSize);
    				updateById(sequence);
    			} else {
    				sequence = new Sequence(sequenceName);
    				sequence.setStep(stepSize);
    				insert(sequence);
    			}
    		}

    		private Sequence readStep(String sequenceName) {
    			return select(sequenceName);
    		}

    		public long getUpperBound() {
    			return sequence.getStep();
    		}
    	}

    	private static class SequencePersisterConfigurer {

    		private DefaultEntityMapping<Sequence, String, SequenceTable> buildConfiguration(PooledHiLoSequenceStorageOptions storageOptions) {
    			// Sequence table creation
    			SequenceTable sequenceTable = new SequenceTable(null, storageOptions.getTable(), storageOptions.getSequenceNameColumn(), storageOptions.getValueColumn());
    			// Strategy building
    			// NB: no id generator here because we manage ids (see reservePool)
    			return new DefaultEntityMapping<>(
    					Sequence.class,
    					sequenceTable,
    					sequenceTable.getPooledSequenceFieldMapping(),
    					Sequence.SEQUENCE_NAME_FIELD,
    					// Setting a "Noop" identifier manager because we use a insert(..), updateById(..) and select(..) for simple case
    					// (no complex graph nor chain of code on persisted instance)
    					new AlreadyAssignedIdentifierManager<>(String.class, c -> {}, c -> true));
    		}
    	}
    }